local userController           = {}
local userModel                = require("models.userModel")
local crypt                    = require("crypt")
local openssl_rand             = require("openssl.rand")
local encoding                 = require("lapis.util.encoding")
local util                     = require("lapis.util")
local email_tool               = require("utils.email_tool")
local article_tool             = require("utils.article_tool")
local storage_avatars_path     = require("utils.website_config").storage_avatars_path
local storage_backgrounds_path = require("utils.website_config").storage_backgrounds_path


-- Formatea los datos del formulario de "registro"
-- y los registra en la base de datos.
function userController:userSignup(data)
	data.password_confirm = nil

	-- Formatea un e-mail a minusculas.
	data.email = string.lower(data.email)

	-- Consulta en la base de datos si existe un usuario registrado con el mismo apodo y/o email.
	local list_users = userModel:userFind(data.username, data.email)

	for user,column in ipairs(list_users) do
		-- Valida si la cuenta de un usuario está activa.
		if column.activated == 1 then
			return "Existe una cuenta registrada con el mismo apodo o dirección de correo electrónico, por favor inténtelo de nuevo."
		else
			-- Valida si el token está expirado y elimina la cuenta del usuario asociado.
			if column.token_expiration ~= "userdata" then
				if os.time() > column.token_expiration then
					userModel:userDelete(column.id)
				else
					return "Existe una cuenta registrada con el mismo apodo o dirección de correo electrónico, por favor inténtelo de nuevo."
				end
			else
				return "Existe una cuenta registrada con el mismo apodo o dirección de correo electrónico, por favor inténtelo de nuevo."
			end
		end
	end

	-- Cifra la contraseña del usuario.
	data.password = crypt.encrypt('sha512', data.password)

	-- Elimina espacios en blanco sobrantes del nombre completo del usuario.
	data.name = string.gsub(data.name, "%s+", " ")

	local validation_key = encoding.encode_base64(openssl_rand.bytes(32))
	
	-- Cifra la llave de verificación aleatoria.
	data.hash = crypt.encrypt('sha512', validation_key)

	-- Construye un token que contiene la dirección e-mail y una llave de verificación aleatoria.
	-- Se utiliza como parámetro de la ruta de verificación para activar cuentas de usuarios por e-mail.
	local token = { email = data.email, key = validation_key }

	-- Codifica los datos almacenados en el token y escapa caracteres especiales.
	token = util.escape(encoding.encode_with_secret(token))

	local url_verification = require("utils.website_config").website_domain .. "/verification/" .. token

	-- Define la estrutura de un e-mail de activación de cuentas.
	local email_struct = {
		destination = data.email,
		name        = data.name,
		subject     = "Verificación de email",
		message     = "<p>¡Hola "..data.username.."! espero que estés bien. "..
					  "Para completar tu registo en "..require("utils.website_config").website_name..
					  ", por favor confirma tu dirección de correo electrónico con el siguiente enlace:</p>"..
					  '<div><a href="'..url_verification..'">Confirmar mi e-mail</a></div>'..
					  "<p>Puedes ignorar este mensaje si no realizaste esta solicitud.</p>"
	}

	if email_tool:send(email_struct) then
		-- Fecha de expiración del token (1 hora).
		data.expiration = os.time() + 60*60

		-- Registra los datos del usuario en la base de datos.
		userModel:userSave(data)

		return "Su cuenta se ha registrado correctamente, por favor confirme su dirección de correo electrónico."
	else
		return "Los sentimos mucho, tuvimos problemas para registrar su cuenta, por favor inténtelo de nuevo."
	end
end


-- Formatea los datos del formulario de "inicio de sesión"
-- y valida sus credenciales de acceso desde la base de datos.
function userController:userLogin(data)
	local user, message, identity_column
	local regex_email = require("utils.website_config").regex_email
	local email_rule  = string.match(data.identity, regex_email)

	-- Selecciona una columna de la tabla "users" dependiendo del tipo de identificación,
	-- si es por apodo o dirección e-email.
	if data.identity == email_rule then
		identity_column = "email"
	else
		identity_column = "username"
	end

	-- Consulta en la base de datos si el usuario está registrado.
	local user_access = userModel:userAccess(identity_column, data.identity)

	if user_access then
		-- Valida la autentificación de la contraseña.
		local validate_password = crypt.check(data.password, user_access.password)

		if validate_password then
			-- Valida si la cuenta está activa.			
			if user_access.activated == 0 then
				-- Valida si el token está expirado y elimina la cuenta del usuario asociado.
				if user_access.token_expiration ~= "userdata" then
					if os.time() > user_access.token_expiration then
						userModel:userDelete(user_access.id)

						message = "Su cuenta ha expirado, por favor vuelva a registrarse."
					else
						message = "Por favor confirma tu dirección de correo electrónico."
					end
				else
					message = "Tu cuenta ha sido bloqueada por uso indebido del servicio."
				end
			else
				user          = {}
				user.id       = user_access.id
				user.role     = user_access.role
				user.username = user_access.username
			end
		else
			message = "La identificación de sesión o la contraseña es incorrecta."
		end
	else
		message = "La identificación de sesión o la contraseña es incorrecta."
	end

	return user, message
end


-- Activa una cuenta de usuario a través de un token de verificación de e-mail.
function userController:userVerification(token)
	local redirect = false
	local message

	-- Convierte los caracteres escapados del token a caracteres especiales.
	token = util.unescape(token)

	-- Decodifica los datos del token.
	local token_data = encoding.decode_with_secret(token)

	if token_data then
		-- Valida y consulta en la base de datos si la dirección de e-mail del token está registrado.
		local user = userModel:userVerification(token_data.email)

		if user then
			if type(user.token_expiration) == "userdata" then
				redirect = true
			else
				-- Valida si el token está expirado y elimina la cuenta del usuario asociado.
				if (os.time() > user.token_expiration) and (user.activated == 0) then
					userModel:userDelete(user.id)

					message  = "Su cuenta ha expirado, por favor vuelva a registrarse."
				else
					-- Valida la autentificación de la llave de verificación aleatoria del token
					-- y activa la cuenta del usuario.
					local validate_password = false

					-- Valida si el hash de verificación no es [NULL] y si es autentica.				
					if type(user.token_hash) ~= "userdata" then
						validate_password = crypt.check(token_data.key, user.token_hash)
					end

					if validate_password then
						userModel:userActive(user.id)

						message = "Su dirección de correo electrónico se ha verificado correctamente."
					else
						redirect = true
					end
				end
			end
		else
			redirect = true
		end
	else
		redirect = true
	end

	return redirect, message
end


-- Retorna los datos requeridos para mostrar un perfil de usuario, utilizando su apodo.
function userController:userProfile(username)
	-- Convierte los caracteres escapados del apodo a caracteres especiales,
	-- modifica a "self.params.username".
	username = util.unescape(username)

	-- Consulta en la base de datos si el perfil de un usuario existe.
	local profile = userModel:userProfile("username", username)

	if profile then
		-- Valida si el campo de la descripción es NULL.
		if type(profile.description) == "userdata" then
			profile.description = nil
		end

		local default_avatar_path     = require("utils.website_config").default_avatar_path
		local default_background_path = require("utils.website_config").default_background_path

		-- Establece una foto de perfil por defecto si el campo es NULL.
		if type(profile.avatar_file) == "userdata" then
			profile.avatar_file = default_avatar_path
		else
			profile.avatar_file = storage_avatars_path..profile.avatar_file
		end

		-- Establece un fondo de perfil por defecto si es NULL.
		if type(profile.background_file) == "userdata" then
			profile.background_file = default_background_path
		else
			profile.background_file = storage_backgrounds_path..profile.background_file
		end
	end

	return profile
end


-- Elimina una imagen anterior y almacena una nueva imagen dado sus rutas y su contenido,
-- dentro del directorio raíz del proyecto, valido únicamente en sistemas POSIX.
local function saveImage(old_path, new_path, content)
	if old_path ~= "" then
		os.remove("."..old_path)
	end
	
	local file = io.open("."..new_path, "w+b")
	file:write(content)
	file:close()
end


-- Edita un perfil de usuario utilizando sus datos de sesión y su apodo.
function userController:userEdit(session, data)
	local profile_columns = {}
	local user_columns    = {}
	local message         = {}

	-- Consulta los datos personales y de perfil del usuario actual.
	local profile = userModel:userProfile("id", session.id)

	-- Valida si el usuario cambió su foto de perfil.
	if data.avatar then
		-- Obtiene la extensión de la foto.
		local avatar_extension = string.gsub(data.avatar["content-type"], "image/", "")

		-- Genera un nombre único a la foto de perfil y consulta en la base de datos si existe,
		-- si existe genera otro nombre.
		local exist_avatar

		repeat
			profile_columns.avatar_file = article_tool.generateName().."."..avatar_extension

			exist_avatar = userModel:imageFind("avatar_file", profile_columns.avatar_file)
		until exist_avatar == nil

		local old_avatar_path = ""
		local new_avatar_path = storage_avatars_path..profile_columns.avatar_file

		-- Valida si existe una foto de perfil almacenado anteriormente,
		-- construye la ruta de la foto de perfil anterior.
		if type(profile.avatar_file) ~= "userdata" then
			old_avatar_path = storage_avatars_path..profile.avatar_file
		end

		-- Almacena la nueva foto de perfil en la carpeta de almacenamiento.
		local state_avatar = pcall(saveImage, old_avatar_path, new_avatar_path, data.avatar.content)

		if state_avatar then
			table.insert(message, "Se modificó correctamente tu foto de perfil.")
		else
			profile_columns.avatar_file = nil

			table.insert(message, "Lo sentimos, por favor intente de nuevo modificar su foto de perfil.")
		end
	end

	-- Valida si el usuario cambió su fondo de perfil.
	if data.background then
		-- Obtiene la extesión de la imagen.
		local background_extension = string.gsub(data.background["content-type"], "image/", "")

		-- Genera un nombre único a la imagen de fondo de perfil y consulta en la base de datos si existe,
		-- si existe genera otro nombre.
		local exist_background

		repeat
			profile_columns.background_file = article_tool.generateName().."."..background_extension

			exist_background = userModel:imageFind("background_file", profile_columns.background_file)
		until exist_background == nil

		local old_background_path = ""
		local new_background_path = storage_backgrounds_path..profile_columns.background_file

		-- Valida si existe un fondo de perfil almacenado anteriormente.
		if type(profile.background_file) ~= "userdata" then
			old_background_path = storage_backgrounds_path..profile.background_file
		end

		-- Almacena la nueva imagen de fondo de perfil en la carpeta de almacenamiento.
		local state_background = pcall(saveImage, old_background_path, new_background_path, data.background.content)

		if state_background then
			table.insert(message, "Se modificó correctamente tu fondo de perfil.")
		else
			profile_columns.background_file = nil

			table.insert(message, "Lo sentimos, por favor intente de nuevo modificar su foto de perfil.")
		end
	end

	-- Valida el usuario cambió su descripción o autobiografia.
	if type(profile.description) == "userdata" then -- Valida si el campo de la descripción en la BD es NULL.
		if data.description ~= "" then -- Valida si el campo del formulario es diferente de vacío.
			profile_columns.description = data.description

			table.insert(message, "Se modificó correctamente tu descripción de perfil.")
		end
	else -- Valida si el campo de la descripción en la BD [NO] es NULL?
		if data.description == "" then -- Valida si el campo del formulario está vacío.
			profile_columns.description = require("lapis.db").NULL -- Establece en NULL el campo de la descripción en la BD.

			table.insert(message, "Se modificó correctamente tu descripción de perfil.")
		else
			if data.description ~= profile.description then -- Valida si el campo del formulario es diferente al campo de la descripción en la BD.
				profile_columns.description = data.description

				table.insert(message, "Se modificó correctamente tu descripción de perfil.")
			end
		end
	end

	-- Guarda los cambios realizados en el perfil del usuario.
	if next(profile_columns) then
		userModel:profileEdit(session.id, profile_columns)
	end

	-- Valida si el usuario cambió su nombre completo.
	if data.name ~= "" then
		if profile.name ~= data.name then
			user_columns.name = data.name
			session.name      = data.name

			table.insert(message, "Se modificó correctamente tu nombre completo.")
		end
	end

	-- Valida si el usuario cambió su apodo y consulta en la base de datos si no existe.
	if data.username ~= "" then
		local exist_user = userModel:userUsername(data.username)

		if exist_user == nil then
			user_columns.username = data.username
			session.username      = data.username

			table.insert(message, "Se modificó correctamente tu apodo.")
		end
	end

	-- Valida si el usuario cambió su contraseña.
	if data.password ~= "" then
		user_columns.password = crypt.encrypt('sha512', data.password)

		table.insert(message, "Se modificó correctamente tu contraseña.")
	end

	-- Guarda los cambios realizados en los datos personales del usuario.
	if next(user_columns) then
		userModel:userEdit(session.id, user_columns)
	end

	if next(message) == nil then
		message = nil
	end

	return message
end


return setmetatable({}, { __index = userController })